在Node中要实现延时一个任务执行有很多种方法:
setTimeout(callback, 0)
setImmediate(callback, 0)
process.nextTick
promise.resolve
此篇文章来分析一下这些方法有什么区别。
Node中的任务分为宏任务(MacroTask)和微任务(MicroTask),在每一个宏任务阶段中间穿插着微任务。
宏任务包含:script全部代码、setTimeout、setInterval、setImmediate、I/O等。
微任务包含:Process.nextTick、Promise等。
由于Node的Event Loop是基于libuv实现的,首先来看一下libuv这一张完整的事件循环阶段图:
执行的阶段如下:
已经到期的timer,也就是 setTimeout 和 setIntervel 。
大多数情况下,在轮询I/O之后立即调用所有I/O回调。但在某些情况下,有些回调会推迟到下一个循环,也就是在这里执行上一次推迟的I/O回调,主要是处理一些系统调用错误,比如网络通信的错误回调。
在阻塞I/O执行前执行idle, prepare句柄。
计算轮询I/O的超时时间(注1)。
根据上一步计算的时间轮询或阻塞I/O循环,文件读写操作等所有I/O相关的回调都会被调用。
调用检查句柄回调,比如 setImmediate
调用关闭句柄回调
由于 setTimeout/setInterval 的第二个参数取值范围是:1 ~ 2^31-1,如果超过这个范围则会初始化为 1,那么就会有:
setTimeout(fn, 0) === setTimeout(fn, 1)
从上面的event loop分析可以看到 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间,所以就会出现两种情况:
timer 前的时间大于1ms,setTimeout的时间就满足了,久先执行setTimeout的回调。
timer 前的时间小于1ms,setTimeout的时间就不满足了,久先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 到 timer 阶段时再去执行setTimeout。
因此 setTimeout 0ms 和 setImmediate 的谁先谁后真的不好说(不过一般来说还是setTimeout更快)。
另外看网上的这个例子:
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
})
因为是在 I/O 的回调里面,那么接下来的阶段就是 check handle call,所以一定是先执行 setImmediate ,再去执行 setTimeout。
const promise = Promise.resolve()
promise.then(() => {
console.log('promise')
});
process.nextTick(() => {
console.log('nextTick')
});
虽然 promise 与 process.nextTick 都是注册到microTask中,但 process.nextTick 总是早于 promise.then 执行,于是输出结果就是:
nextTick
promise
那么再回到最开始的问题
const promise = Promise.resolve()
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
});
promise.then(() => {
console.log('promise')
});
process.nextTick(() => {
console.log('nextTick')
});
这四个方法谁先谁后呢?通过上面的分析可以得到结论:
nextTick
promise
setTimeout
setImmediate
注1:
如果使用 UV:mdps:&:split:mdps:ref:prefix:0:mdps:&:split:NOWAIT 标志运行,超时时间为0
如果 uv_stop 已经调用了,要停止轮询,超时时间为0
没有活动的句柄或请求,超时时间为0
有任何idle句柄处于活动状态,超时时间为0
如果有任何要关闭的句柄,超时时间为0
如果以上情况均不匹配,如果有timer,那么用距离最近的timer的时间,否则为无穷大